<?php

/**
 * Created by PhpStorm.
 * User: Administrator
 * Date: 2017/2/28
 * Time: 14:38
 */
class MsggNexmo extends MsggChannelBase
{
    use util_array;
    const HTTP_RESP_CODE_RESOURCE_CREATE = 201;
    const HTTP_RESP_CODE_NO_CONTENT = 204;
    const HTTP_RESP_CODE_UNAUTHORIZED = 401;
    const HTTP_RESP_CODE_NOT_FOUND = 404;
    const HTTP_RESP_CODE_RATE_LIMITED = 429;
    const HTTP_RESP_CODE_NEXMO_SERVER_ERROR = 500;

    const CALL_TIMEOUT_TIME = 2 * 60;

    // const BASE_URL = 'https://api.nexmo.com/v1/';
    // with china dns
    const BASE_URL = 'https://api.nexmocn.com/v1/';

    const CONFIG_REQUEST_TIMEOUT = 'request_timeout';
    const CONFIG_REQUEST_RATE = 'request_rate';

    const TYPE_SMS = 'sms';
    const TYPE_CALL = 'call';

    const DEFAULT_KEY = 'default';

    const ERROR_CODE_FAIL = 1;
    const ERROR_CODE_OVERRATE = 2;

    private static $_conf;

    public static function sms($params = array(), $conf = array())
    {
        static::$_conf = $conf;
        $result = array('code' => 0, 'msg' => '', 'data' => array(), 'ext' => array());
        try {
            $query = array(
                'to' => ((substr($params['phone'], 0, 2) === '86') ? $params['phone'] : '86' . $params['phone']),
                'from' => 'bsc',
                "type" => 'unicode',
                'text' => vsprintf($params['template']['content'], $params['param']),
                "callback" => MsggHook::getUrl(__CLASS__, 'hookSms'),
            );
            if (!($query['api_key'] = self::getConf('key'))) {
                throw new Exception('api key invalid', self::ERROR_CODE_FAIL);
            }
            if (!($query['api_secret'] = self::getConf('secret'))) {
                throw new Exception('api secret invalid', self::ERROR_CODE_FAIL);
            }
            $url = 'https://rest.nexmo.com/sms/json?' . http_build_query($query);

            $try = self::getConf('try', 2);
            while ($try) {
                $try--;
                $cres = Curl::get($url);
                if ($cres['err']) {
                    if ($try) {
                        continue;
                    }
                    throw new Exception('CurlErr:' . $cres['err'], self::ERROR_CODE_FAIL);
                }
                if (empty($cres['content'])) {
                    if ($try) {
                        continue;
                    }
                    throw new Exception('response content empty', self::ERROR_CODE_FAIL);
                }
                if (!($resp = json_decode($cres['content'], true))) {
                    if ($try) {
                        continue;
                    }
                    throw new Exception('response content invalid:' . $cres['content'], self::ERROR_CODE_FAIL);
                }
                $respCode = isset($resp['messages'][0]['status']) ? $resp['messages'][0]['status'] : '-';
                if ($respCode != 0) {
                    if ($try) {
                        continue;
                    }
                    throw new Exception('response code abnormal:' . $respCode, self::ERROR_CODE_FAIL);
                }
                $result['data'] = $resp;
                $result['ext']['id'] = isset($resp['messages'][0]['message-id']) ? $resp['messages'][0]['message-id'] : null;;
                $result['ext']['status'] = MsggSms::STATUS_RUNNING;
                break;
            }
        } catch (Exception $e) {
            $result['code'] = self::ERROR_CODE_FAIL;
            $result['msg'] = $e->getMessage();
            $result['ext']['status'] = MsggSms::STATUS_ERROR;
        }
        $result['ext']['class'] = self::class;
        return $result;
    }

    public static function call($params = array(), $conf = array())
    {
        static::$_conf = $conf;
        $result = array('code' => 0, 'msg' => '', 'data' => array(), 'ext' => array());
        try {
            if (!self::addRate(self::TYPE_CALL, $params, $conf)) {
                throw new Exception('nexmo call over rate', self::ERROR_CODE_OVERRATE);
            }

            $url = self::BASE_URL . 'calls/';
            $headers = array(
                'Content-Type' => 'application/json',
                'Authorization' => 'Bearer ' . self::getJWT(),
            );
            $number = '';
            if (substr($params['phone'], 0, 1) === '+') {
                $number = substr($params['phone'], 1);
            } elseif (substr($params['phone'], 0, 2) === '00') {
                $number = substr($params['phone'], 2);
            } else {
                $number = '86' . $params['phone'];
            }
            $time = time();
            $fromPhone = self::getConf('from_phone', '85258039361');
            $size = is_array($fromPhone) ? sizeof($fromPhone) : 0;
            $index = 0;
            if ($size && ($cache = Yii::app()->cache)) {
                $cacheValue = $cache->get('nexmo_from_phone_index');
                $index = $cacheValue % $size;
                $cache->set('nexmo_from_phone_index', ($index + 1) % $size, 12 * 3600);
            }
            $fromNum = is_array($fromPhone) ? $fromPhone[$index] : $fromPhone;
            Yii::log($time . 'phnoe:' . $fromNum, CLogger::LEVEL_INFO, __METHOD__);
            $postdata = array(
                'to' => array(
                    array(
                        'type' => 'phone',
                        'number' => $number,
                    )
                ),
                'from' => array(
                    'type' => 'phone',
                    'number' => $fromNum,
                ),
                'answer_url' => array(
                    MsggHook::getUrl(__CLASS__, 'hookAnswer', array('media' => $params['media']['name']))
                ),
                'event_url' => array(MsggHook::getUrl(__CLASS__, 'hookCall')),
                'event_method' => 'GET',
                'ringing_timer' => 45,
            );
            $try = self::getConf('try', 2);
            while ($try) {
                $try--;
                $cres = Curl::post($url, json_encode($postdata, JSON_UNESCAPED_UNICODE), array(), $headers, 3);
                if ($cres['err']) {
                    if ($try) {
                        continue;
                    }
                    throw new Exception('CurlErr:' . $cres['err'], self::ERROR_CODE_FAIL);
                }
                $httpCode = isset($cres['header']['http_code']) ? $cres['header']['http_code'] : '-';
                if ($httpCode != '201') {
                    if ($try && $httpCode != self::HTTP_RESP_CODE_NEXMO_SERVER_ERROR) {
                        continue;
                    }
                    throw new Exception('response http code invalid:' . $httpCode, self::ERROR_CODE_FAIL);
                }
                if (empty($cres['content'])) {
                    //over rate is stop to retry
                    if ($try) {
                        continue;
                    }
                    throw new Exception('response content empty', self::ERROR_CODE_FAIL);
                }
                if (!($resp = json_decode($cres['content'], true))) {
                    if ($try) {
                        continue;
                    }
                    throw new Exception('response content invalid:' . $cres['content'], self::ERROR_CODE_FAIL);
                }
                $result['data'] = $resp;
                $result['ext']['id'] = empty($resp['uuid']) ? null : $resp['uuid'];
                $result['ext']['status'] = MsggCall::STATUS_RUNNING;
                break;
            }
        } catch (Exception $e) {
            $result['code'] = self::ERROR_CODE_FAIL;
            $result['msg'] = $e->getMessage();
            $result['ext']['status'] = MsggCall::STATUS_ERROR;
        }
        //reduce rate
        self::reduceRate(self::TYPE_CALL);
        //return result
        $result['ext']['class'] = self::class;
        return $result;
    }

    private static function getJWT()
    {
        $now = time();
        if (!($application_id = self::getConf('application_id'))) {
            throw new Exception('application id invalid', self::ERROR_CODE_FAIL);
        }
        if (!($application_key = self::getConf('application_key'))) {
            throw new Exception('application key invalid', self::ERROR_CODE_FAIL);
        }
        $header = array(
            'typ' => 'JWT',
            'alg' => 'RS256'
        );
        $payload = array(
            'iat' => $now,
            'exp' => $now + 60,
            'jti' => base64_encode(mt_rand()),
            'application_id' => $application_id,
        );
        $jwt = array(
            'header' => self::base64UrlEncode(json_encode($header)),
            'payload' => self::base64UrlEncode(json_encode($payload)),
        );
        $data = implode('.', array_values($jwt));
        $key = openssl_get_privatekey($application_key);
        if ($key === false) {
            throw new Exception('key fail: ' . openssl_error_string(), self::ERROR_CODE_FAIL);
        }
        $details = openssl_pkey_get_details($key);
        if (!isset($details['key']) || $details['type'] !== OPENSSL_KEYTYPE_RSA) {
            throw new Exception('key fail: not compatible with RSA signatures' . openssl_error_string(),
                self::ERROR_CODE_FAIL);
        }
        if (!openssl_sign($data, $signature, $key, OPENSSL_ALGO_SHA256)) {
            throw new Exception('sign fail:' . openssl_error_string(), self::ERROR_CODE_FAIL);
        }
        $jwt['signature'] = self::base64UrlEncode($signature);
        return implode('.', array_values($jwt));
    }

    private static function addRate($type, $params = [], $config = [])
    {
        $addRate = true;
        if (empty($params) || empty($config)) {
            $addRate = true;
        } else {
            switch ($type) {
                case self::TYPE_SMS:
                case self::TYPE_CALL:
                    $time = time();
                    $timeout = self::getConf(self::CONFIG_REQUEST_TIMEOUT, 2);
                    $rate = self::getConf(self::CONFIG_REQUEST_RATE, null);
                    $addRate = $rate == null ? true : false;
                    while (time() - $time <= $timeout && $rate != null && !$addRate) {
                        $lockFile = MsggItemValve::getLockFile(implode('_', [$type, self::class]));
                        $fp = fopen($lockFile, 'w');
                        if (flock($fp, LOCK_EX)) {
                            if ($cache = Yii::app()->cache) {
                                $value = $cache->get(implode('_', [$type, self::class]), 0);
                                $value = !$value ? 0 : $value;
                                if ($value < $rate) {
                                    Yii::log($value . 'value lower rate ', CLogger::LEVEL_INFO, __METHOD__);
                                    $value++;
                                    $cache->set(implode('_', [$type, self::class]), $value, $timeout);
                                    $addRate = true;
                                }
                            }
                            flock($fp, LOCK_SH);
                        }
                        fclose($fp);
                        if ($addRate) {
                            break;
                        }
                        Yii::log('sleep for 1 second', CLogger::LEVEL_WARNING, __METHOD__);
                        sleep(1);
                    }
                    break;
            }
        }
        return $addRate;
    }

    private static function reduceRate($type)
    {
        $lockFile = MsggItemValve::getLockFile(implode('_', [$type, self::class]));
        $fp = fopen($lockFile, 'w');
        if (flock($fp, LOCK_EX)) {
            if ($cache = Yii::app()->cache) {
                $value = $cache->get(implode('_', [$type, self::class]), 0);
                $value = !$value ? 0 : $value;
                if ($value) {
                    Yii::log('reduce value' . $value, CLogger::LEVEL_INFO, __METHOD__);
                    $value--;
                    $cache->set(implode('_', [$type, self::class]), $value);
                }
            }
            flock($fp, LOCK_SH);
        }
        fclose($fp);
    }

    private static function base64UrlEncode($data)
    {
        return str_replace('=', '', strtr(base64_encode($data), '+/', '-_'));
    }

    private static function getConf($name, $default = null)
    {
        return isset(static::$_conf[$name]) ? static::$_conf[$name] : $default;
    }


    public static function hookAnswer($params)
    {
        header('Content-type:application/json');
        $data = array(
            array(
                'action' => 'stream',
                'streamUrl' => array(MsggMedia::getUrl(empty($params['media']) ? self::DEFAULT_KEY : $params['media'])),
                'loop' => 2,
            )
        );
        echo json_encode($data);
    }

    public static function hookSms($params)
    {
        if (!($ext_id = empty($params['messageId']) ? null : (string)$params['messageId'])) {
            return;
        }
        if (!($status = empty($params['status']) ? null : (string)$params['status'])) {
            return;
        }
        if (!($ext_status = self::mapStatus(self::TYPE_SMS, $status))) {
            return;
        }
        MsggChannelLog::updateExtStatus($ext_id, $ext_status, $params);
    }

    public static function hookCall($params)
    {
        if (!($ext_id = empty($params['uuid']) ? null : (string)$params['uuid'])) {
            return;
        }
        if (!($status = empty($params['status']) ? null : (string)$params['status'])) {
            return;
        }
        // if this time is failed and the status in the time before this time is ringing  we can tread this call is rejected
        if (($channelLog = MsggChannelLog::model()->findByAttributes(['ext_id' => $ext_id])) != null) {
            $hooks = $channelLog->msgItemHooks;
            foreach ($hooks as $hook) {
                $hookParams = json_decode($hook->params, true);
                $hookStatus = self::util_array_get($hookParams, 'status', null);
                if ($hookStatus == 'ringing' && $status == 'failed') {
                    $status = 'rejected';
                    break;
                }
            }
        }

        if ($status == 'complete' && ($price = self::util_array_get($params, 'price', 0)) && $price != 0) {
            $status = 'answered';
        }
        if (!($ext_status = self::mapStatus(self::TYPE_CALL, $status))) {
            return;
        }
        MsggChannelLog::updateExtStatus($ext_id, $ext_status, $params);
    }

    private static function mapStatus($type, $status)
    {
        $resStatus = null;
        switch ($type) {
            case self::TYPE_SMS:
                switch ($status) {
                    case 'buffered':
                        $resStatus = MsggSms::STATUS_RUNNING;
                        break;
                    case 'delivered':
                        $resStatus = MsggSms::STATUS_SUCCESS_DELIVERED;
                        break;
                    case 'accepted':
                        $resStatus = MsggSms::STATUS_SUCCESS;
                        break;
                    case 'rejected':
                        $resStatus = MsggSms::STATUS_FAILURE;
                        break;
                    case 'failed':
                    case 'unknown':
                    case 'expired':
                        $resStatus = MsggSms::STATUS_FAILURE;
                        break;
                }
                break;
            case self::TYPE_CALL:
                switch ($status) {
                    case 'started':
                    case 'ringing':
                        $resStatus = MsggCall::STATUS_RUNNING;
                        break;
                    case 'answered':
                        $resStatus = MsggCall::STATUS_SUCCESS;
                        break;
                    case 'cancelled':
                    case 'rejected':
                        $resStatus = MsggCall::STATUS_COMPLETE_REJECT;
                        break;
                    case 'unanswered':
                        $resStatus = MsggCall::STATUS_COMPLETE_UNANSWERED;
                        break;
                    case 'busy':
                        $resStatus = MsggCall::STATUS_COMPLETE_BUSY;
                        break;
                    case 'timeout':
                        $resStatus = MsggCall::STATUS_COMPLETE_TIMEOUT;
                        break;
                    case 'failed':
                    case 'machine':
                        $resStatus = MsggCall::STATUS_FAILURE;
                        break;
                }
                break;
        }
        return $resStatus;
    }

    public static function refreshStatus(MsggChannelLog $channelLog)
    {
        // TODO: Implement refreshStatus() method.
        $url = self::BASE_URL;
        $time = $channelLog->time;
        if (empty($channelLog)) {
            return 'error';
        }
        switch ($channelLog->type) {
            case MsggItem::TYPE_CALL:
                $uuid = $channelLog->ext_id;
                self::$_conf = json_decode($channelLog->conf, true);
                $headers = array(
                    'Content-Type' => 'application/json',
                    'Authorization' => 'Bearer ' . self::getJWT(),
                );
                $url = ($url . "calls" . "/" . $uuid);
                $cres = Curl::get($url, [], $headers);
                $content = self::util_array_get($cres, 'content', []);
                if (!is_array($content)) {
                    $content = json_decode($content, true);
                }
                $status = self::util_array_get($content, 'status', null);
                $price = self::util_array_get($content, 'price', 0);
                if ($price > 0) {
                    Yii::log('nexmo call success', CLogger::LEVEL_INFO, __METHOD__);
                    $ext_status = MsggCall::STATUS_SUCCESS;
                } elseif ($status == 'complete' || $status == 'ringing') {
                    Yii::log('nexmo call complete ', CLogger::LEVEL_INFO, __METHOD__);
                    $ext_status = MsggCall::STATUS_COMPLETE_UNANSWERED;
                } elseif ($status == 'failed' || time() - $time >= self::CALL_TIMEOUT_TIME) {
                    Yii::log('nexmo call failed ', CLogger::LEVEL_INFO, __METHOD__);
                    $ext_status = MsggCall::STATUS_FAILURE;
                }else{
                    $ext_status = self::mapStatus(self::TYPE_CALL, $status);
                }
                return $ext_status;
                break;
        }
        return parent::refreshStatus($channelLog);
    }
}